package jam4j.lang;

import jam4j.build.Target;
import jam4j.util.FallibleThunk;

import java.util.*;

/**
 * Complete state of the interpreter. Most parts of the Context are internal to
 * the interpreter.
 *
 * @author Luke Maurer
 */
public final class Context {
    private final Deque<Scope> scopes = new ArrayDeque<Scope>();
    private final Map<String, Scope> targetScopes =
        new HashMap<String, Scope>();
    private final Map<String, Rule> rules = new HashMap<String, Rule>();
    private final Map<String, Target> targets = new HashMap<String, Target>();
    
    Context() {
        scopes.push(new Scope());
    }

    /**
     * Get the initial Context. All variables passed in as the parameter will
     * be preset, as well as all special variables; and all builtin rules will
     * be predefined.
     * 
     * @param settings A map of variables to set at startup; these may include
     * those taken from environment variables, system properties, or the
     * command line.
     * 
     * @return A new Context, as exists at startup.
     */
    public static Context initialContext(Map<String, String[]> settings) {
        final Context ans = new Context();
        Builtins.register(ans.rules);
        
        ans.globalScope().putAll(settings);
        
        for (SpecialVariable var : SpecialVariable.values()) {
            final String[] value = var.initialValue();
            if (value != null)
                ans.set(var.toString(), value);
        }
        
        return ans;
    }

    /**
     * Get the current value of a variable.
     * 
     * @param name The name of the variable.
     * @return Its value, as an array of strings.
     */
    public String[] get(String name) {
        if ("<".equals(name))
            name = "1";
        else if (">".equals(name))
            name = "2";

        for (Scope scope : scopes) {
            final String[] ans;
            if ((ans = scope.get(name)) != null)
                return ans;
        }

        return Expression.UNDEF;
    }

    /**
     * Find whether a variable has a value.
     * 
     * @param name The name of the variable.
     * @return Whether the variable has a value. Note that an empty value is
     *         considered the same as none at all, so setting a variable to the
     *         empty list effectively undefines it.
     */
    public boolean isSet(String name) {
        return get(name).length > 0;
    }
    
    void set(String name, String ... values) {
        scopeWith(name).set(name, values);
    }

    void setAppend(String name, String ... values) {
        scopeWith(name).setAppend(name, values);
    }

    void setDefault(String name, String ... values) {
        scopeWith(name).setDefault(name, values);
    }
    
    void set(Target target, String name, String ... values) {
        if (target == null)
            set(name, values);
        else
            scope(target).set(name, values);
    }
    
    void setAppend(Target target, String name, String ... values) {
        if (target == null)
            setAppend(name, values);
        else
            scope(target).setAppend(name, values);
    }
    
    void setDefault(Target target, String name, String ... values) {
        if (target == null)
            setDefault(name, values);
        else
            scope(target).setDefault(name, values);
    }

    void setLocal(String name, String ... values) {
        localScope().set(name, values);
    }

    void makeLocal(String name) {
        localScope().makeLocal(name);
    }

    private Scope scopeWith(String name) {
        final Scope globalScope = globalScope();
        for (Scope scope : scopes)
            if (scope == globalScope || scope.containsKey(name))
                return scope;
        throw new IllegalStateException("No global scope?!");
    }

    Scope localScope() {
        return scopes.getFirst();
    }

    Scope globalScope() {
        return scopes.getLast();
    }

    Scope targetScope(String name) {
        Scope ans;
        
        if ((ans = targetScopes.get(name)) == null)
            targetScopes.put(name, ans = new Scope());
        
        return ans;
    }
    
    Scope scope(Target target) {
        return targetScope(target.name());
    }
    
    Rule rule(String name) {
        Rule ans;
        
        if ((ans = rules.get(name)) == null)
            rules.put(name, ans = new Rule(name));
        
        return ans;
    }

    String[] call(String name, String onTarget, String[] ... arguments) {
        return rule(name).call(this, target(onTarget), arguments);
    }

    /**
     * Get a target, creating it if it does not yet exist.
     * 
     * @param name The name of the target.
     * @return The target, having been created if necessary.
     */
    public Target target(String name) {
        if (name == null)
            return null;
        
        final Target ans;
        if ((ans = targets.get(name)) == null) { 
            final Target target = new Target(name);
            targets.put(name, target);
            return target;
        } else
            return ans;
    }
    
    Target[] targets(String ... args) {
        final int count = args.length;
        final Target[] ans = new Target[count];
        for (int ix = 0; ix < count; ix++)
            ans[ix] = target(args[ix]);
        return ans;
    }

    private Scope enterScope(Scope scope) {
        scopes.push(scope);
        return scope;
    }

    Scope enterScope() {
        return enterScope(new Scope());
    }

    Scope enterOnTarget(Target target) {
        return (target == null ? enterScope() : 
            enterScope(new Scope(scope(target))));
    }
    
    private String[] boundTargetNames(Target ... targets) {
        final int count = targets.length;
        final String[] ans = new String[count];
        
        for (int ix = 0; ix < count; ix++)
            ans[ix] = targets[ix].bind(this).boundName();
        
        return ans;
    }
    
    private String[] boundTargetNames(String ... names) {
        final int count = names.length;
        final Target[] targets = new Target[count];
        for (int ix = 0; ix < count; ix++)
            targets[ix] = target(names[ix]);
        
        return boundTargetNames(targets);
    }
    
    Scope enterOnBoundTargets(
            String[] boundTargets) {
        final Scope scope = enterScope();
        
        try {
            for (String targetVar : boundTargets)
                scope.put(targetVar, boundTargetNames(this.get(targetVar)));
            
            return scope;
        } catch (RuntimeException e) {
            leaveScope();
            throw e;
        }
    }

    void leaveScope() {
        if (scopes.size() < 2)
            throw new IllegalStateException("Cannot leave global scope");

        scopes.pop();
    }
    
    /**
     * Perform an operation in a new scope with the given target's variables
     * set.
     * 
     * @param <T> The type that the operation returns.
     * @param <X> An exception type that may be thrown by the operation.
     * @param target The target whose variables will be brought into scope.
     * @param thunk The operation to perform.
     * @return Whatever the operation returns.
     * @throws X If thrown by the operation.
     * 
     * @see jam4j.util.FallibleThunk
     * @see jam4j.util.Thunk
     */
    public <T, X extends Throwable> T runOnTarget(
            Target target, FallibleThunk<T, X> thunk) throws X {
        enterOnTarget(target);
        try {
            return thunk.call();
        } finally {
            leaveScope();
        }
    }
    
    String[][] values(Expression ... args) {
        final List<String[]> ans = new ArrayList<String[]>();
        for (Expression arg : args)
            ans.add(arg.resolve(this));
        return ans.toArray(new String[ans.size()][]);
    }
    
    @Override
    public String toString() {
        return "Context " + scopes;
    }
}
